﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Diagnostics;
using Common.Logging;
using System.Reflection;

namespace Util.Props
{
    /// <summary>
    /// Manages a set of <code>IConfigurable</code>s, their parameterization and the relationships between them. Configurations
    /// can be specified either by xml or on-the-fly during runtime. 
    ///
    /// 
    /// </summary>
    public class ConfigurationManager: Object,ICloneable
    {
        private List<IConfigurationChangeListener> changeListeners = new List<IConfigurationChangeListener>();

        private Dictionary<String, PropertySheet> symbolTable = new Dictionary<String, PropertySheet>();
        private Dictionary<String, RawPropertyData> rawPropertyMap = new Dictionary<String, RawPropertyData>();
        private Dictionary<String, String> globalProperties = new Dictionary<String, String>();

        private Boolean showCreations=false;
        private String configURL = String.Empty;

        /// <summary>
        /// Creates a new empty configuration manager. This constructor is only of use in cases when a system configuration
        /// is created during runtime.
        /// </summary>
        public ConfigurationManager()
        {
        }

        /// <summary>
        /// Creates a new configuration manager. Initial properties are loaded from the given URL. No need to keep the notion
        /// of 'context' around anymore we will just pass around this property manager.
        /// </summary>
        /// <param name="configFileName"></param>
        public ConfigurationManager(String url)
        {
            configURL = url;

            try 
            {
                rawPropertyMap = new SaxLoader(url, globalProperties).load();
            } 
            catch (IOException e) 
            {
                throw new SystemException(e.ToString());
            }

            ConfigurationManagerUtils.applySystemProperties(rawPropertyMap, globalProperties);
            ConfigurationManagerUtils.configureLogger(this);

            // we can't configure the configuration manager with itself so we
            // do some of these configure items manually.
            if (globalProperties.ContainsKey("showCreations"))
            {
                String showCreations = globalProperties["showCreations"];
                if (showCreations != null)
                    this.showCreations = "true".Equals(showCreations);
            }
        }



        public Object Clone()
        {
            ConfigurationManager cloneCM = (ConfigurationManager)new Object();

            cloneCM.changeListeners = new List<IConfigurationChangeListener>();
            cloneCM.symbolTable = new Dictionary<String, PropertySheet>();
            foreach (KeyValuePair<String, PropertySheet> entry in symbolTable) 
            {
                cloneCM.symbolTable.Add(entry.Key, ((PropertySheet)entry.Value.Clone()));
            }

            cloneCM.globalProperties = new Dictionary<String, String>(globalProperties);
            cloneCM.rawPropertyMap = new Dictionary<String, RawPropertyData>(rawPropertyMap);


            return cloneCM;
        }

        /// <summary>
        /// Returns the URL of the XML configuration which defined this configuration or <code>null</code>  if it was created
        /// dynamically.
        /// </summary>
        /// <returns></returns>
        public String getConfigURL()
        {
            return configURL;
        }

        /// <summary>
        /// Returns a global property.
        /// </summary>
        /// <param name="propertyName"></param>
        /// <returns></returns>
        public String getGlobalProperty(String propertyName)
        {
            //        propertyName = propertyName.startsWith("$") ? propertyName : "${" + propertyName + "}";
            String globProp = globalProperties[propertyName];
            return globProp != null ? globProp.ToString() : null;
        }

        public IConfigurable lookup(String instanceName) 
        {
            // Apply all new properties to the model.
            instanceName = getStrippedComponentName(instanceName);
            PropertySheet ps = null;
            try
            {
                ps = getPropertySheet(instanceName);
            }
            catch(Exception ex)
            {
                Trace.WriteLine(ex.ToString());
                return null;
            }
            if (ps == null)
                return null;

            if (showCreations)
                Trace.WriteLine("Creating: " + instanceName);

            return ps.getOwner();
        }

        public String getStrippedComponentName(String propertyName) 
        {
            Trace.Assert(propertyName != null);

            while (propertyName.StartsWith("$"))
                propertyName = globalProperties[ConfigurationManagerUtils.stripGlobalSymbol(propertyName)].ToString();

            return propertyName;
        }

        /// <summary>
        /// Returns the property sheet for the given object instance
        /// </summary>
        /// <param name="instanceName"></param>
        /// <returns></returns>
        public PropertySheet getPropertySheet(String instanceName) 
        {
            instanceName = instanceName.ToLower();
            if (!symbolTable.ContainsKey(instanceName)) 
            {
                // if it is not in the symbol table, so construct
                // it based upon our raw property data
                if (!rawPropertyMap.ContainsKey(instanceName))
                {
                    String msg = "rawPropertyMap not contain configuration information for instance: " + instanceName;
                    Trace.WriteLine(msg);
                    throw new InternalConfigurationException(instanceName, "rawPropertyMap", msg);
                }
                RawPropertyData rpd = rawPropertyMap[instanceName];
                String asemblyName = String.Empty;
                if (rpd != null) 
                {
                    String className = rpd.className;
                    try {
                        asemblyName = className.Substring(0, className.IndexOf('.'));
                        Assembly loadAssembly = Assembly.Load(asemblyName);

                        if (loadAssembly == null)
                            throw new FileNotFoundException("Can't locate the assembly: " + asemblyName);

                        Type loadType = loadAssembly.GetType(className);
                        if (loadType == null)
                            throw new ArgumentException("Assembly not contain the required type:" + className);


                        IConfigurable cls = (IConfigurable)loadAssembly.CreateInstance(className);
                        // now load the property-sheet by using the class annotation
                        PropertySheet propertySheet = new PropertySheet(cls, instanceName, this, rpd);

                        symbolTable.Add(instanceName, propertySheet);

                    } 
                    catch (FileNotFoundException e) 
                    {
                        Trace.Fail("Class:" + className + ": assembly :" + asemblyName + " is not found.");
                        throw e;
                    } 
                }
            }

            return symbolTable[instanceName];
        }

        /// <summary>
        /// Creates an instance of the given <code>Configurable</code> by using the default parameters as defined by the
        /// class annotations to parameterize the component.
        /// </summary>
        /// <param name="targetClass"></param>
        /// <returns></returns>
        public static IConfigurable getInstance(IConfigurable targetClass) 
        {
            return getInstance(targetClass, new Dictionary<String, Object>());
        }


        /// <summary>
        /// Creates an instance of the given <code>Configurable</code> by using the default parameters as defined by the
        /// class annotations to parameterize the component. Default parameters will be overridden if a their names are
        /// contained in the given <code>props</code>-map
        /// </summary>
        /// <param name="targetClass"></param>
        /// <param name="props"></param>
        /// <returns></returns>
        public static IConfigurable getInstance(IConfigurable targetClass, Dictionary<String, Object> props)
        {
            return getInstance(targetClass, props, null);
        }

        /// <summary>
        /// Creates an instance of the given <code>Configurable</code> by using the default parameters as defined by the
        /// class annotations to parameterize the component. Default parameters will be overridden if a their names are
        /// contained in the given <code>props</code>-map. The component is used to create a parameterized logger for the
        /// Configurable being created.
        /// </summary>
        /// <param name="targetClass"></param>
        /// <param name="props"></param>
        /// <param name="compName"></param>
        /// <returns></returns>
        public static IConfigurable getInstance(IConfigurable targetClass, Dictionary<String, Object> props, String compName)
        {
            PropertySheet ps = getPropSheetInstanceFromClass(targetClass, props, compName, new ConfigurationManager());
            IConfigurable configurable = ps.getOwner();
            return targetClass as IConfigurable;
        }


        private static PropertySheet getPropSheetInstanceFromClass(IConfigurable targetClass, Dictionary<String, Object> defaultProps, String componentName, ConfigurationManager cm) 
        {
            RawPropertyData rpd = new RawPropertyData(componentName, targetClass.GetType().Name);

            foreach (KeyValuePair<String,Object> entry in  defaultProps) 
            {
                Object property = entry.Value;

                if (property is IConfigurable)
                    property = ((IConfigurable) property).GetType().Name;

                rpd.properties.Add(entry.Key, property);
            }

            return new PropertySheet(targetClass, componentName, cm, rpd);
        }

        public String getGloPropReference(String propertyName)
        {
            return globalProperties[propertyName];
        }

        /// <summary>
        /// Returns all names of configurables registered to this instance. The resulting set includes instantiated and
        /// non-instantiated components.
        /// </summary>
        /// <returns></returns>
        public List<String> getComponentNames()
        {
            List<String> ret = new List<String>();
            if (rawPropertyMap.Keys != null && rawPropertyMap.Keys.Count > 0)
                ret = rawPropertyMap.Keys.ToList<String>();
            return ret;
        }

        /// <summary>
        /// Informs all registered <code>ConfigurationChangeListener</code>s about a configuration changes the component
        /// named <code>configurableName</code>.
        /// </summary>
        /// <param name="configurableName"></param>
        /// <param name="propertyName"></param>
        public void fireConfChanged(String configurableName, String propertyName) 
        {
            Trace.Assert(getComponentNames().Contains(configurableName.ToLower()));

            foreach (IConfigurationChangeListener changeListener in changeListeners)
                changeListener.configurationChanged(configurableName, propertyName, this);
        }

        /**
        /// Sets a global property.
         *
        /// @param propertyName The name of the global property.
        /// @param value        The new value of the global property. If the value is <code>null</code> the property becomes
        ///                     removed.
         */
        public void setGlobalProperty(String propertyName, String value) 
        {
            if (value == null)
                globalProperties.Remove(propertyName);
            else
                globalProperties.Add(propertyName, value);

            // update all component configurations because they might be affected by the change
            foreach (String instanceName in getInstanceNames(typeof(IConfigurable))) 
            {
                PropertySheet ps = getPropertySheet(instanceName);
                if (ps.isInstanciated())
                    try {
                        ps.getOwner().newProperties(ps);
                    } 
                    catch (PropertyException e) 
                    {
                        Trace.WriteLine(e.Message);
                    }
            }
        }

        /**
        /// Gets all instances that are of the given type.
         *
        /// @param type the desired type of instance
        /// @return the set of all instances
         */
        public List<String> getInstanceNames(Type _type) 
        {
            List<String> instanceNames = new List<String>();

            foreach (PropertySheet ps in symbolTable.Values) 
            {
                if (!ps.isInstanciated())
                    continue;

                if (ConfigurationManagerUtils.isDerivedClass(ps.getConfigurableClass(), _type))
                    instanceNames.Add(ps.InstanceName);
            }

            return instanceNames;
        }

        

    }
}
